package io.gitee.thinkbungee.crud.mongo.utils;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import io.gitee.thinkbungee.crud.mongo.reflection.SerializableFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.Introspector;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 解析函数表达式工具类
 *
 * @author chenyichang
 * @date 2022/5/11
 */
public class FunctionResolveUtils {
    private static final Logger log = LoggerFactory.getLogger(FunctionResolveUtils.class);
    /**
     * 缓存
     */
    public static final Map<SerializableFunction<?, ?>, Field> cache = new ConcurrentHashMap<>();

    /**
     * 解析函数表达式
     */
    public static <T, R> String resolve(SerializableFunction<T, R> function) {
        return getField(function).getName();
    }

    /**
     * 从缓存获取
     */
    public static Field getField(SerializableFunction<?, ?> function) {
        return cache.computeIfAbsent(function, FunctionResolveUtils::findField);
    }

    /**
     * 找到该属性
     * <p>
     * 例子：findField(SatelliteConstant.Mongo::getId)
     * <p>
     * 原理分析： https://blog.csdn.net/iteye_19045/article/details/119299015
     * 我们知道实现了序列化接口的java对象是可以被序列化的（用于IO传输、持久化等），但是真正被序列化的其实只有对象的属性，而方法（即函数）不能被序列化，可lamba
     * 表达式实际上是一个函数（函数式编程），那么“函数”通过什么方式来序列化呢？ Java提供了一种机制，会将实现了Serializable接口的lambda表达式转换成 SerializedLambda 对象之后再去做序列化。
     * <p>
     * function（lambda表达式）实际上也是一个类，只是这个类是由虚拟机生成的，并且实现了Function接口
     * 方法除了 apply()，还多一个writeReplace方法。（因为实现了Serializable接口）
     * <p>
     * writeReplace方法 是由虚拟机加上的，lambda方法必须实现Serializable接口，否则会报错
     * <p>
     * Java序列化机制
     * 虚拟机在调用write(obj)序列化对象前，如果被序列化的对象有writeReplace方法，则会先调用该方法，
     * 用该方法返回的SerializedLambda对象去做序列化，即被序列化的对象被替换了。
     * <p>
     * 根据这个原理，lambda表达式SatelliteConstant.Mongo::getId在序列化前也会调用writeReplace()，然后返回一个SerializedLambda
     * 对象（真正的被序列化的对象），该对象中包含了lambda表达式的所有信息，
     * 比如
     * 函数名implMethodName、
     * 函数签名implMethodSignature 等等，
     * 由于这些信息都是以字段形式存在的，因此可以被序列化，这样就解决了函数无法被序列化的问题。
     * <p>
     * 所以根据这个原理，我们也可以自己通过反射来获取序列化后的对象
     * <p>
     * mybatis-plus中也是类似先获取 SerializedLambda
     */
    private static Field findField(SerializableFunction<?, ?> function) {
        Field field = null;
        String fieldName = null;
        try {
            //获取SerializedLambda
            Method method = function.getClass().getDeclaredMethod("writeReplace");
            method.setAccessible(Boolean.TRUE);
            SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);

            //implMethodName 就是Field对应的Getter方法名
            String implMethodName = serializedLambda.getImplMethodName();
            if (implMethodName.startsWith("get") && implMethodName.length() > 3) {
                fieldName = Introspector.decapitalize(implMethodName.substring(3));
            } else if (implMethodName.startsWith("is") && implMethodName.length() > 2) {
                fieldName = Introspector.decapitalize(implMethodName.substring(2));
            } else {
                throw new IllegalArgumentException(implMethodName + "不是Getter方法引用");
            }

            //获取的Class是字符串，并且包名是“/”分割，需要替换成“.”，才能获取到对应的Class对象
            String declaredClass = serializedLambda.getImplClass().replace("/", ".");
            Class<?> aClass = Class.forName(declaredClass, false, ClassUtil.getClassLoader());

            //反射获取class定义的field
            field = ReflectUtil.getField(aClass, fieldName);

        } catch (Exception e) {
            log.error("解析函数属性值失败", e);
        }
        if (field != null) {
            return field;
        }

        throw new NoSuchFieldError(fieldName);
    }
}
